/*
 * Copyright (C) 2025 Huawei Device Co., Ltd.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

import {
  constants,
  expectedNGConstants,
  Http2SessionStateIndex,
  kAlpnProtocol,
  kBoundSession,
  kEncrypted,
  kHandle,
  kLocalSettings,
  kMaxStreams,
  kMaybeDestroy,
  kNativeFields,
  kProxySocket,
  kRemoteSettings,
  kServer,
  kSocket,
  kState,
  kTimeout,
  kType,
  kUpdateTimer,
  proxySocketHandler,
  SESSION_FLAGS_CLOSED,
  SESSION_FLAGS_DESTROYED,
  SESSION_FLAGS_PENDING,
  SESSION_FLAGS_READY
} from "../constant/Constant";
import { EventEmitter } from "../event/EventEmitter";
import { hilog } from "@kit.PerformanceAnalysisKit";
import {
  debugSession,
  debugSessionObj,
  initOriginSet,
  setupHandle,
  validateBuffer,
  validateFunction,
  validateInt32,
  validateNumber
} from "../utils";
import {LogUtils} from "../LogUtils";

export class Http2Session extends EventEmitter {
  constructor(type, options?, socket?) {
    super();
    // No validation is performed on the input parameters because this
    // constructor is not exported directly for users.

    // If the session property already exists on the socket,
    // then it has already been bound to an Http2Session instance
    // and cannot be attached again.

    if (socket[kBoundSession] !== undefined) {
      LogUtils.getInstance().error('constructor ERR_HTTP2_SOCKET_BOUND');
      throw new Error('ERR_HTTP2_SOCKET_BOUND');
    }

    socket[kBoundSession] = this;

    this[kState] = {
      destroyCode: constants.NGHTTP2_NO_ERROR,
      flags: SESSION_FLAGS_PENDING,
      goawayCode: null,
      goawayLastStreamID: null,
      streams: new Map(),
      pendingStreams: new Set(),
      pendingAck: 0,
      shutdownWritableCalled: false,
      writeQueueSize: 0,
      originSet: undefined,
    };

    this[kEncrypted] = undefined;
    this[kAlpnProtocol] = undefined;
    this[kType] = type;
    this[kProxySocket] = null;
    this[kSocket] = socket;
    this[kTimeout] = null;
    this[kHandle] = undefined;

    // Do not use nagle's algorithm
    if (typeof socket.setNoDelay === 'function') {
      socket.setNoDelay();
    }

    // Disable TLS renegotiation on the socket
    if (typeof socket.disableRenegotiation === 'function') {
      socket.disableRenegotiation();
    }

    if (socket.connecting || socket.secureConnecting) {
      const connectEvent =
        socket instanceof socket.TLSSocket ? 'secureConnect' : 'connect';
      socket.once(connectEvent, () => {
        try {
          setupHandle(this, socket, type, options);
        } catch (error) {
          socket.destroy(error);
        }
      });
    } else {
      setupHandle(this, socket, type, options);
    }
    debugSession(type, 'created');
  }

  // Returns undefined if the socket is not yet connected, true if the
  // socket is a TLSSocket, and false if it is not.
  get encrypted() {
    return this[kEncrypted];
  }

  // Returns undefined if the socket is not yet connected, `h2` if the
  // socket is a TLSSocket and the alpnProtocol is `h2`, or `h2c` if the
  // socket is not a TLSSocket.
  get alpnProtocol() {
    return this[kAlpnProtocol];
  }

  // TODO(jasnell): originSet is being added in preparation for ORIGIN frame
  // support. At the current time, the ORIGIN frame specification is awaiting
  // publication as an RFC and is awaiting implementation in nghttp2. Once
  // added, an ORIGIN frame will add to the origins included in the origin
  // set. 421 responses will remove origins from the set.
  get originSet() {
    if (!this.encrypted || this.destroyed) {
      return undefined;
    }
    return Array.from(initOriginSet(this));
  }

  // True if the Http2Session is still waiting for the socket to connect
  get connecting() {
    return (this[kState].flags & SESSION_FLAGS_READY) === 0;
  }

  // True if Http2Session.prototype.close() has been called
  get closed() {
    return !!(this[kState].flags & SESSION_FLAGS_CLOSED);
  }

  // True if Http2Session.prototype.destroy() has been called
  destroyed() {
    return !!(this[kState].flags & SESSION_FLAGS_DESTROYED);
  }

  closeSession(session, code, error) {
    closeSession(session, code, error)
  }

  // Resets the timeout counter
  [kUpdateTimer]() {
    if (this.destroyed) {
      return;
    }
    if (this[kTimeout]) {
      this[kTimeout].refresh();
    }
  }

  // Sets the id of the next stream to be created by this Http2Session.
  // The value must be a number in the range 0 <= n <= kMaxStreams. The
  // value also needs to be larger than the current next stream ID.
  setNextStreamID(id) {
    if (this.destroyed) {
      LogUtils.getInstance().error('ERR_HTTP2_INVALID_SESSION');
      throw new Error('ERR_HTTP2_INVALID_SESSION');
    }

    validateNumber(id, 'id');
    if (id <= 0 || id > kMaxStreams) {
      LogUtils.getInstance().error(`ERR_OUT_OF_RANGE(${id} > 0 and <= ${kMaxStreams}`);
      throw new Error(`ERR_OUT_OF_RANGE(${id} > 0 and <= ${kMaxStreams}`);
    }

    this[kHandle].setNextStreamID(id);
  }

  // Sets the local window size (local endpoints's window size)
  // Returns 0 if success or throw an exception if NGHTTP2_ERR_NOMEM
  // if the window allocation fails
  setLocalWindowSize(windowSize) {
    if (this.destroyed) {
      LogUtils.getInstance().error('ERR_HTTP2_INVALID_SESSION');
      throw new Error('ERR_HTTP2_INVALID_SESSION');
    }

    validateInt32(windowSize, 'windowSize', 0);
    const ret = this[kHandle].setLocalWindowSize(windowSize);

    if (ret === expectedNGConstants.NGHTTP2_ERR_NOMEM) {
      LogUtils.getInstance().error('ERR_HTTP2_NO_MEM');
      throw new Error('ERR_HTTP2_NO_MEM');
    }
  }

  // If ping is called while we are still connecting, or after close() has
  // been called, the ping callback will be invoked immediately with a ping
  // cancelled error and a duration of 0.0.

  ping(payload?, callback?) {
    if (this.destroyed) {
      LogUtils.getInstance().error('ERR_HTTP2_INVALID_SESSION');
      throw new Error('ERR_HTTP2_INVALID_SESSION');
    }

    if (typeof payload === 'function') {
      callback = payload;
      payload = undefined;
    }
    if (payload) {
      validateBuffer(payload, 'payload');
    }
    if (payload && payload.length !== 8) {
      throw new Error('ERR_HTTP2_PING_LENGTH');
    }
    validateFunction(callback, 'callback');

    const cb = pingCallback(callback);
    if (this.connecting || this.closed) {
      return;
    }

    return this[kHandle].ping(payload, cb);
  }
  // The socket owned by this session
  get socket() {
    const proxySocket = this[kProxySocket];
    if (proxySocket === null) {
      return this[kProxySocket] = new Proxy(this, proxySocketHandler);
    }
    return proxySocket;
  }

  // The session type
  get type() {
    return this[kType];
  }

  // If a GOAWAY frame has been received, gives the error code specified
  get goawayCode() {
    return this[kState].goawayCode || expectedNGConstants.NGHTTP2_NO_ERROR;
  }

  // If a GOAWAY frame has been received, gives the last stream ID reported
  get goawayLastStreamID() {
    return this[kState].goawayLastStreamID || 0;
  }

  // True if the Http2Session is waiting for a settings acknowledgement
  get pendingSettingsAck() {
    return this[kState].pendingAck > 0;
  }

  // Retrieves state information for the Http2Session
  get state() {
    return this.connecting || this.destroyed ?
      { localWindowSize: 0, remoteWindowSize: 0 } : getSessionState(this[kHandle]);
  }

  // The settings currently in effect for the local peer. These will
  // be updated only when a settings acknowledgement has been received.
  get localSettings() {
    const settings = this[kLocalSettings];
    if (settings !== undefined) {
      return settings;
    }

    if (this.destroyed || this.connecting) {
      return {};
    }

    return this[kLocalSettings] = getSettings(this[kHandle], false); // Local
  }

  // The settings currently in effect for the remote peer.
  get remoteSettings() {
    //TODO: bind from bit
    return this[kRemoteSettings] = getSettings(this[kHandle], true); // Remote
  }

  // Submits a SETTINGS frame to be sent to the remote peer.
  settings(settings, callback?) {

    if (callback) {
      validateFunction(callback, 'callback');
    }
    debugSessionObj(this, 'sending settings');

    this[kState].pendingAck++;

    const settingsFn = submitSettings.bind(this, { ...settings }, callback);
    if (this.connecting) {
      // this.once('connect', settingsFn);
      return;
    }
    settingsFn();
  }

  // Submits a GOAWAY frame to be sent to the remote peer. Note that this
  // is only a notification, and does not affect the usable state of the
  // session with the notable exception that new incoming streams will
  // be rejected automatically.
  goaway(code = constants.NGHTTP2_NO_ERROR, lastStreamID = 0, opaqueData) {
    if (this.destroyed) {
      LogUtils.getInstance().error('ERR_HTTP2_INVALID_SESSION');
      throw new Error('ERR_HTTP2_INVALID_SESSION');
    }


    if (opaqueData !== undefined) {
      validateBuffer(opaqueData, 'opaqueData');
    }
    validateNumber(code, 'code');
    validateNumber(lastStreamID, 'lastStreamID');
  }

  // Destroy the Http2Session, making it no longer usable and cancelling
  // any pending activity.

  destroy(error?: number | ESObject, code?: number) {
    if (this.destroyed) {
      return;
    }

    debugSessionObj(this, 'destroying');

    if (typeof error === 'number') {
      code = error;
      error =
        code !== constants.NGHTTP2_NO_ERROR ?
          code : undefined;
    }
    if (code === undefined && error != null) {
      code = constants.NGHTTP2_INTERNAL_ERROR;
    }

    closeSession(this, code, error);
  }

  // Closing the session will:
  // 1. Send a goaway frame
  // 2. Mark the session as closed
  // 3. Prevent new inbound or outbound streams from being opened
  // 4. Optionally register a 'close' event handler
  // 5. Will cause the session to automatically destroy after the
  //    last currently open Http2Stream closes.
  //
  // Close always assumes a good, non-error shutdown (NGHTTP_NO_ERROR)
  //
  // If the session has not connected yet, the closed flag will still be
  // set but the goaway will not be sent until after the connect event
  // is emitted.
  close(callback?) {
    if (this.closed || this.destroyed) {
      return;
    }
    debugSessionObj(this, 'marking session closed');
    this[kState].flags |= SESSION_FLAGS_CLOSED;
    if (typeof callback === 'function') {
      callback()
    }
  }

  // Destroy the session if:
  // * error is not undefined/null
  // * session is closed and there are no more pending or open streams
  [kMaybeDestroy](error) {
    if (error == null) {
      const state = this[kState];
      // Do not destroy if we're not closed and there are pending/open streams
      if (!this.closed ||
        state.streams.size > 0 ||
        state.pendingStreams.size > 0) {
        return;
      }
    }
    // this.destroy(error);
  }

  _onTimeout() {
    LogUtils.getInstance().error('_onTimeout')
    // callTimeout(this, this);
  }

  ref() {
    // if (this[kSocket]) {
    //   this[kSocket].ref();
    // }
  }

  unref() {
    // if (this[kSocket]) {
    //   this[kSocket].unref();
    // }
  }
}


export function closeSession(session, code, error) {
  debugSessionObj(session, 'start closing/destroying', error);

  const state = session[kState];
  state.flags |= SESSION_FLAGS_DESTROYED;
  state.destroyCode = code;

  // Clear timeout and remove timeout listeners.
  session.setTimeout(0);
  session.removeAllListeners('timeout');

  // Destroy any pending and open streams
  if (state.pendingStreams.size > 0 || state.streams.size > 0) {
    const cancel = error;
    state.pendingStreams.forEach((stream) => stream.destroy(cancel));
    state.streams.forEach((stream) => stream.destroy(error));
  }

  // Disassociate from the socket and server.
  const socket = session[kSocket];
  const handle = session[kHandle];

  // Destroy the handle if it exists at this point.
  if (handle !== undefined) {
    handle.ondone = finishSessionClose.bind(null, session, error);
    handle.destroy(code, socket.destroyed);
  } else {
    finishSessionClose(session, error);
  }
}

export function finishSessionClose(session, error) {
  debugSessionObj(session, 'finishSessionClose');
  const socket = session[kSocket];
  cleanupSession(session);
  if (error) {
    session.nextTickEmit('error', { data: [{ "message": error }] })
    return;
  }
  if (!socket) {
    session.nextTickEmit('close', { data: [{ "rstCode": 0 }] })
  }
}

function cleanupSession(session) {
  const socket = session[kSocket];
  const handle = session[kHandle];
  session[kProxySocket] = undefined;
  session[kSocket] = undefined;
  session[kHandle] = undefined;
  if (handle) {
    handle.ondone = null;
  }
  if (socket) {
    socket[kBoundSession] = undefined;
    socket[kServer] = undefined;
  }
}

export function submitSettings(settings, callback) {
  if (this.destroyed) {
    return;
  }
  debugSessionObj(this, 'submitting settings');
  this[kUpdateTimer]();
  //TODO: bind from c-code
  // updateSettingsBuffer(settings);
  // if (!this[kHandle].settings(settingsCallback.bind(this, callback))) {
  //   this.destroy(new ERR_HTTP2_MAX_PENDING_SETTINGS_ACK());
  // }
}

export function getSettings(session, remote) {
  if (remote) {
    session.remoteSettings();
  } else {
    session.localSettings();
  }

  // TODO: bind from c-code
  // const toRet = {
  //   headerTableSize: settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE],
  //   enablePush: !!settingsBuffer[IDX_SETTINGS_ENABLE_PUSH],
  //   initialWindowSize: settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE],
  //   maxFrameSize: settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE],
  //   maxConcurrentStreams: settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS],
  //   maxHeaderListSize: settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE],
  //   maxHeaderSize: settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE],
  //   enableConnectProtocol:
  //   !!settingsBuffer[IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL],
  // };
  // if (settingsBuffer[IDX_SETTINGS_FLAGS + 1]) toRet.customSettings = addCustomSettingsToObj();
  return null;
}

export const sessionState = new Array(Http2SessionStateIndex.IDX_SESSION_STATE_COUNT);

export function getSessionState(session) {
  session.refreshState();
  LogUtils.getInstance().error('no way to getSessionState');
  return {
    effectiveLocalWindowSize:
    sessionState[Http2SessionStateIndex.IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE],
    effectiveRecvDataLength:
    sessionState[Http2SessionStateIndex.IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH],
    nextStreamID:
    sessionState[Http2SessionStateIndex.IDX_SESSION_STATE_NEXT_STREAM_ID],
    localWindowSize:
    sessionState[Http2SessionStateIndex.IDX_SESSION_STATE_LOCAL_WINDOW_SIZE],
    lastProcStreamID:
    sessionState[Http2SessionStateIndex.IDX_SESSION_STATE_LAST_PROC_STREAM_ID],
    remoteWindowSize:
    sessionState[Http2SessionStateIndex.IDX_SESSION_STATE_REMOTE_WINDOW_SIZE],
    outboundQueueSize:
    sessionState[Http2SessionStateIndex.IDX_SESSION_STATE_OUTBOUND_QUEUE_SIZE],
    deflateDynamicTableSize:
    sessionState[Http2SessionStateIndex.IDX_SESSION_STATE_HD_DEFLATE_DYNAMIC_TABLE_SIZE],
    inflateDynamicTableSize:
    sessionState[Http2SessionStateIndex.IDX_SESSION_STATE_HD_INFLATE_DYNAMIC_TABLE_SIZE],
  };
}

export function pingCallback(cb) {
  return function pingCallback(ack, duration, payload) {
    if (ack) {
      cb(null, duration, payload);
    } else {
      cb(new Error('ERR_HTTP2_PING_CANCEL'));
    }
  };
}